<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>跟随鼠标绘制矩形框</title>
    <style>
      #canvas {
        border: 1px solid red;
      }
    </style>
  </head>

  <body>
    <!-- 1、通过width和height属性设置的宽高控制的是canvas元素在页面中占据空间的大小和画布的大小； -->
    <!-- 2、通过css设置的宽高控制的是canvas元素在页面中占据空间的大小，不是画布实际的大小； -->
    <!-- 通过width和height属性设置的宽高会使得canvas元素在页面中占据空间的大小与画布大小一致；通过css设置的宽高则会导致canvas元素在页面中占据空间的大小与画布大小不一致； -->
    <canvas id="canvas" width="640" height="480"></canvas>
    <select name="rect" id="mode">
      <option value="point">标记关键点</option>
      <option value="rect">绘制矩形框</option>
    </select>
    <button id="undo" onclick="undo()">撤销</button>

    <script>
      const canvas = document.getElementById('canvas'),
        context = canvas.getContext('2d'),
        pointArray = [],
        history = [];

      let dragging = false,
        mode = 'rect',
        mousedown = null;

      function Point(x, y, type) {
        this.x = x;
        this.y = y;
        this.type = type; // 左击 1  右击 3
      }

      // 坐标转化为canvas坐标
      function windowToCanvas(x, y, type) {
        //返回元素的大小以及位置
        var bbox = canvas.getBoundingClientRect();
        // bbox 的宽度会加上 canvas 的 border 会影响精度
        return new Point(
          x - bbox.left * (canvas.width / bbox.width),
          y - bbox.top * (canvas.height / bbox.height),
          type
        );
      }

      function drawPoint(point) {
        context.save();
        context.fillStyle = point['type'] === 3 ? 'red' : 'green';
        context.beginPath();
        context.arc(point.x, point.y, 3, 0, Math.PI * 2, true);
        context.fill();
        context.font = '20px serif';
        context.fillText(pointArray.length.toString(), point.x - 5, point.y - 10);
        context.restore();
        pointArray.push(point);
      }

      function updateRect(point) {
        let w = Math.abs(point.x - mousedown.x);
        let h = Math.abs(point.y - mousedown.y);

        let left = point.x > mousedown.x ? mousedown.x : point.x;
        let top = point.y > mousedown.y ? mousedown.y : point.y;

        context.save();
        context.beginPath();
        context.rect(left, top, w, h);
        context.stroke();
        context.restore();
      }

      function showLastHistory() {
        context.putImageData(history[history.length - 1]['data'], 0, 0);
      }

      function undo() {
        if (history.length > 1) {
          history[history.length - 1]['mode'] === 'point' && pointArray.pop();
          history.pop();
          showLastHistory();
        }
      }

      function addHistoy(data) {
        history.push({
          mode,
          data: context.getImageData(0, 0, canvas.width, canvas.height),
        });
      }

      document.getElementById('mode').onchange = function (e) {
        mode = e.target.value;
      };

      // 鼠标事件
      canvas.onmousedown = function (e) {
        e.preventDefault();
        mousedown = windowToCanvas(e.clientX, e.clientY, e.which);
        dragging = true;
      };

      canvas.onmousemove = function (e) {
        e.preventDefault();
        if (dragging && mode === 'rect') {
          // 只有绘制矩形框时有效果
          showLastHistory(); // 每次绘制先清除上一次
          // context.clearRect(0, 0, canvas.width, canvas.height);
          updateRect(windowToCanvas(e.clientX, e.clientY, e.which));
        }
      };
      addHistoy(); // 添加一张默认的数据
      canvas.onmouseup = function (e) {
        e.preventDefault();
        dragging = false;
        mode === 'point' && drawPoint(mousedown);
        addHistoy(); // 保存上一次数据
      };
      // 阻止页面的右击菜单栏
      canvas.oncontextmenu = function (e) {
        e.preventDefault();
      };
    </script>
  </body>
</html>
